<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图立得图片裁剪与照片压缩工具</title>
<style>
body{font-family:"Microsoft YaHei",sans-serif;background:#f0f2f5;margin:0;padding:20px;color:#333}
.container{max-width:900px;margin:0 auto;background:#fff;padding:20px;border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,0.1)}
h2{margin:0 0 15px 0;font-size:18px;border-bottom:1px solid #eee;padding-bottom:10px}
.row{display:flex;flex-wrap:wrap;gap:10px;margin-bottom:10px;align-items:center}
.ctrl-group{display:flex;align-items:center;gap:5px;background:#f9f9f9;padding:4px 8px;border-radius:4px;border:1px solid #eee}
label{font-size:13px;white-space:nowrap;color:#555}
select,input[type=number]{padding:4px;border:1px solid #ddd;border-radius:4px;height:28px;box-sizing:border-box}
input[type=number]{width:70px}
input[type=checkbox]{width:16px;height:16px;margin:0 4px;cursor:pointer}
button{padding:5px 15px;cursor:pointer;background:#1890ff;color:#fff;border:none;border-radius:4px;font-size:13px;transition:all 0.2s}
button:hover{background:#40a9ff}
button:disabled{background:#f5f5f5;color:#ccc;cursor:not-allowed}
button.secondary{background:#fff;border:1px solid #d9d9d9;color:#333}
button.secondary:hover{color:#40a9ff;border-color:#40a9ff}
#editor-area{position:relative;width:100%;height:500px;background:#e1e1e1;margin-bottom:15px;user-select:none;display:flex;align-items:center;justify-content:center;border:1px dashed #aaa;overflow:hidden}
canvas{max-width:100%;max-height:100%;display:block;cursor:default}
.preview-section{background:#fafafa;padding:15px;border-radius:4px;border:1px solid #eee}
.preview-info{font-size:14px;margin-bottom:8px;color:#333;font-weight:bold;font-family:monospace}
#result-img{max-width:100%;max-height:600px;border:1px solid #ddd;background-image:linear-gradient(45deg,#eee 25%,transparent 25%,transparent 75%,#eee 75%,#eee),linear-gradient(45deg,#eee 25%,transparent 25%,transparent 75%,#eee 75%,#eee);background-size:20px 20px;background-position:0 0,10px 10px}
.hidden{display:none!important}
</style>
</head>
<body>
<div class="container">
<h2>图立得照片图片裁剪压缩工具</h2>
<div class="row">
<div class="ctrl-group">
<label>预设尺寸:</label>
<select id="preset-size">
<option value="custom">自定义</option>
<option value="295,413">1寸 (295x413)</option>
<option value="413,579">2寸 (413x579)</option>
<option value="413,626">大1寸 (33x48mm)</option>
<option value="390,567">护照 (33x48mm 400dpi)</option>
<option value="500,500">头像 (500x500)</option>
<option value="800,800">电商主图 (800x800)</option>
<option value="1280,720">720P (16:9)</option>
<option value="1920,1080">1080P (16:9)</option>
</select>
</div>
<div class="ctrl-group">
<label>输出W:</label><input type="number" id="inp-w" value="295">
<label>H:</label><input type="number" id="inp-h" value="413">
<div style="border-left:1px solid #ddd;padding-left:8px;display:flex;align-items:center">
<input type="checkbox" id="lock-ratio" checked><label for="lock-ratio">锁定比例</label>
</div>
</div>
</div>
<div class="row">
<input type="file" id="file-input" accept="image/*" style="display:none">
<button onclick="document.getElementById('file-input').click()">📁 选择图片</button>
<div class="ctrl-group">
<button class="secondary" id="btn-rl" title="左转">↺</button>
<button class="secondary" id="btn-rr" title="右转">↻</button>
<button class="secondary" id="btn-fh" title="水平翻转">⇆</button>
<button class="secondary" id="btn-fv" title="垂直翻转">⇅</button>
</div>
<div class="ctrl-group">
<label>缩放:</label>
<input type="number" id="scale-percent" value="100" min="1" style="width:50px">
<label>%</label>
</div>
<button id="btn-download" disabled>⬇ 下载结果图</button>
</div>
<div id="editor-area">
<div id="msg-tip" style="color:#888;pointer-events:none">请先选择图片进行裁剪</div>
<canvas id="canvas"></canvas>
</div>
<div class="preview-section">
<div class="preview-info" id="res-info">W: - | H: - | Size: -</div>
<img id="result-img">
</div>
</div>
<script>
const $=(id)=>document.getElementById(id);
// state.imgDataUrl holds the base64 output for download
const state={img:null,rot:0,flipH:1,flipV:1,crop:{x:0,y:0,w:0,h:0},drag:{active:false,mode:'',sx:0,sy:0,startCrop:null},canvas:{scale:1,ox:0,oy:0},imgDataUrl:''};
const els={input:$('file-input'),preset:$('preset-size'),w:$('inp-w'),h:$('inp-h'),lock:$('lock-ratio'),scalePercent:$('scale-percent'),cvs:$('canvas'),ctx:$('canvas').getContext('2d'),dl:$('btn-download'),resImg:$('result-img'),resInfo:$('res-info'),area:$('editor-area'),msg:$('msg-tip')};
const HANDLE_SIZE=8; const TOUCH_HIT=20;

function init(){
    els.input.onchange=handleFile;
    els.preset.onchange=applyPreset;
    els.w.oninput=()=>updateRatio(true);
    els.h.oninput=()=>updateRatio(true);
    els.scalePercent.oninput=generateResult;
    $('btn-rl').onclick=()=>transform(-90,1,1);
    $('btn-rr').onclick=()=>transform(90,1,1);
    $('btn-fh').onclick=()=>transform(0,-1,1);
    $('btn-fv').onclick=()=>transform(0,1,-1);
    els.dl.onclick=download;
    // Mouse
    els.cvs.onmousedown=dragStart;
    window.onmousemove=dragMove;
    window.onmouseup=dragEnd;
    // Touch
    els.cvs.ontouchstart=(e)=>{if(e.touches.length===1){dragStart(e);e.preventDefault();}};
    window.ontouchmove=(e)=>{if(e.touches.length===1){dragMove(e);}};
    window.ontouchend=dragEnd;
}

function handleFile(e){
    if(!e.target.files[0])return;
    const reader=new FileReader();
    reader.onload=(ev)=>{
        state.img=new Image();
        state.img.onload=()=>{
            state.rot=0;state.flipH=1;state.flipV=1;
            els.dl.disabled=false; els.msg.classList.add('hidden');
            initCrop(); draw(); generateResult();
        };
        state.img.src=ev.target.result;
    };
    reader.readAsDataURL(e.target.files[0]);
}

function applyPreset(){
    if(els.preset.value==='custom')return;
    const [w,h]=els.preset.value.split(',').map(Number);
    els.w.value=w; els.h.value=h;
    els.lock.checked=true;
    if(state.img){ initCrop(); draw(); generateResult(); }
}

function updateRatio(fromInput){
    els.preset.value='custom';
    if(state.img && fromInput){
        initCrop(); draw(); generateResult();
    }
}

function transform(r,fh,fv){
    if(!state.img)return;
    if(r) state.rot=(state.rot+r)%360;
    if(fh<0) state.flipH*=-1;
    if(fv<0) state.flipV*=-1;
    initCrop(); draw(); generateResult();
}

function getDim(){
    if(!state.img)return {w:0,h:0};
    const rad=state.rot*Math.PI/180;
    const s=Math.sin(rad), c=Math.cos(rad);
    const w=Math.abs(state.img.width*c)+Math.abs(state.img.height*s);
    const h=Math.abs(state.img.width*s)+Math.abs(state.img.height*c);
    return {w,h};
}

function initCrop(){
    const dim=getDim();
    const aspect=parseInt(els.w.value)/parseInt(els.h.value);
    const imgAspect=dim.w/dim.h;
    let cw, ch;
    if(imgAspect>aspect){
        ch=dim.h; cw=dim.h*aspect;
    }else{
        cw=dim.w; ch=dim.w/aspect;
    }
    cw*=0.95; ch*=0.95;
    state.crop={x:(dim.w-cw)/2, y:(dim.h-ch)/2, w:cw, h:ch};
}

function draw(){
    if(!state.img)return;
    const dim=getDim();
    const cvs=els.cvs;
    const ctx=els.ctx;
    const aw=els.area.clientWidth, ah=els.area.clientHeight;
    state.canvas.scale = Math.min(aw/dim.w, ah/dim.h)*0.9;
    const sw=state.canvas.scale;
    cvs.width=dim.w*sw; cvs.height=dim.h*sw;
    ctx.save();
    ctx.translate(cvs.width/2, cvs.height/2);
    ctx.rotate(state.rot*Math.PI/180);
    ctx.scale(state.flipH*sw, state.flipV*sw);
    ctx.drawImage(state.img, -state.img.width/2, -state.img.height/2);
    ctx.restore();
    const c=state.crop;
    const cx=c.x*sw, cy=c.y*sw, cw=c.w*sw, ch=c.h*sw;
    ctx.fillStyle='rgba(0,0,0,0.5)';
    ctx.beginPath();
    ctx.rect(0,0,cvs.width,cvs.height);
    ctx.rect(cx,cy,cw,ch);
    ctx.clip('evenodd');
    ctx.fill();
    ctx.strokeStyle='#fff'; ctx.lineWidth=1; ctx.strokeRect(cx,cy,cw,ch);
    ctx.strokeStyle='#1890ff'; ctx.setLineDash([5,5]); ctx.strokeRect(cx,cy,cw,ch);
    ctx.setLineDash([]);
    drawHandle(ctx, cx+cw/2, cy, 'n'); 
    drawHandle(ctx, cx+cw/2, cy+ch, 's'); 
    drawHandle(ctx, cx, cy+ch/2, 'w'); 
    drawHandle(ctx, cx+cw, cy+ch/2, 'e'); 
    ctx.fillStyle='#1890ff';
    ctx.font='12px sans-serif';
    ctx.textBaseline='bottom';
    ctx.textAlign='right';
    ctx.fillText(`${Math.round(c.w)} x ${Math.round(c.h)}`, cx+cw-5, cy+ch-5);
}

function drawHandle(ctx,x,y,type){
    const s=HANDLE_SIZE;
    ctx.fillStyle='#fff'; ctx.strokeStyle='#1890ff'; ctx.lineWidth=1;
    ctx.fillRect(x-s/2,y-s/2,s,s);
    ctx.strokeRect(x-s/2,y-s/2,s,s);
}

function getPos(e){
    const rect=els.cvs.getBoundingClientRect();
    let cx, cy;
    if(e.touches && e.touches.length > 0){
        cx = e.touches[0].clientX;
        cy = e.touches[0].clientY;
    } else {
        cx = e.clientX;
        cy = e.clientY;
    }
    return {
        x:(cx-rect.left)/state.canvas.scale,
        y:(cy-rect.top)/state.canvas.scale,
        rawX:cx-rect.left,
        rawY:cy-rect.top
    };
}

function dragStart(e){
    if(!state.img)return;
    const p=getPos(e);
    const c=state.crop;
    const s=state.canvas.scale;
    const hits=[
        {d:'n', x:c.x+c.w/2, y:c.y},
        {d:'s', x:c.x+c.w/2, y:c.y+c.h},
        {d:'w', x:c.x, y:c.y+c.h/2},
        {d:'e', x:c.x+c.w, y:c.y+c.h/2}
    ];
    let mode=null;
    for(let h of hits){
        const hx=h.x*s, hy=h.y*s;
        if(Math.abs(p.rawX-hx)<TOUCH_HIT && Math.abs(p.rawY-hy)<TOUCH_HIT){
            mode=h.d; break;
        }
    }
    if(!mode){
        if(p.x>c.x && p.x<c.x+c.w && p.y>c.y && p.y<c.y+c.h) mode='move';
    }
    if(mode){
        state.drag={active:true, mode:mode, sx:p.x, sy:p.y, startCrop:{...c}};
        els.cvs.style.cursor='grabbing';
    }
}

function dragMove(e){
    if(!state.drag.active){
        const p=getPos(e); const c=state.crop; const s=state.canvas.scale;
        let cur='default';
        if(p.x>c.x&&p.x<c.x+c.w&&p.y>c.y&&p.y<c.y+c.h) cur='move';
        const mx=p.x, my=p.y, tol=TOUCH_HIT/s;
        if(Math.abs(mx-(c.x+c.w/2))<tol && Math.abs(my-c.y)<tol) cur='ns-resize';
        if(Math.abs(mx-(c.x+c.w/2))<tol && Math.abs(my-(c.y+c.h))<tol) cur='ns-resize';
        if(Math.abs(mx-c.x)<tol && Math.abs(my-(c.y+c.h/2))<tol) cur='ew-resize';
        if(Math.abs(mx-(c.x+c.w))<tol && Math.abs(my-(c.y+c.h/2))<tol) cur='ew-resize';
        els.cvs.style.cursor=cur;
        return;
    }
    e.preventDefault && e.preventDefault();
    const p=getPos(e);
    const d=state.drag;
    const sc=d.startCrop;
    const dim=getDim();
    const dx=p.x-d.sx;
    const dy=p.y-d.sy;
    const locked=els.lock.checked;
    const ratio=sc.w/sc.h;
    
    let nc={...sc};
    
    if(d.mode==='move'){
        nc.x = Math.max(0, Math.min(sc.x+dx, dim.w-sc.w));
        nc.y = Math.max(0, Math.min(sc.y+dy, dim.h-sc.h));
    } else {
        const clampW = (w) => Math.max(10, Math.min(w, dim.w));
        const clampH = (h) => Math.max(10, Math.min(h, dim.h));
        
        if(d.mode==='e'){
            nc.w = clampW(sc.w + dx);
            if(nc.x + nc.w > dim.w) nc.w = dim.w - nc.x;
            if(locked){
                nc.h = nc.w / ratio;
                nc.y = sc.y - (nc.h - sc.h)/2; 
                if(nc.y < 0) { nc.y=0; if(nc.y+nc.h > dim.h) nc.h=dim.h; } 
                else if(nc.y+nc.h > dim.h) { nc.y=dim.h-nc.h; if(nc.y<0) {nc.y=0; nc.h=dim.h;} }
                nc.w = nc.h * ratio;
            }
        } else if(d.mode==='w'){
            let effDx = Math.max(-sc.x, dx);
            effDx = Math.min(effDx, sc.w - 10);
            nc.x = sc.x + effDx;
            nc.w = sc.w - effDx;
            if(locked){
                nc.h = nc.w / ratio;
                nc.y = sc.y - (nc.h - sc.h)/2;
                if(nc.y < 0) { nc.y=0; nc.h=Math.min(nc.h, dim.h); nc.w=nc.h*ratio; nc.x=sc.x+(sc.w-nc.w); }
                else if(nc.y+nc.h > dim.h) { nc.y=Math.max(0, dim.h-nc.h); nc.h=dim.h-nc.y; nc.w=nc.h*ratio; nc.x=sc.x+(sc.w-nc.w); }
            }
        } else if(d.mode==='s'){
            nc.h = clampH(sc.h + dy);
            if(nc.y + nc.h > dim.h) nc.h = dim.h - nc.y;
            if(locked){
                nc.w = nc.h * ratio;
                nc.x = sc.x - (nc.w - sc.w)/2; 
                if(nc.x < 0) { nc.x=0; nc.w=Math.min(nc.w, dim.w); nc.h=nc.w/ratio; }
                else if(nc.x+nc.w > dim.w) { nc.x=Math.max(0, dim.w-nc.w); nc.w=dim.w-nc.x; nc.h=nc.w/ratio; }
            }
        } else if(d.mode==='n'){
            let effDy = Math.max(-sc.y, dy);
            effDy = Math.min(effDy, sc.h - 10);
            nc.y = sc.y + effDy;
            nc.h = sc.h - effDy;
            if(locked){
                nc.w = nc.h * ratio;
                nc.x = sc.x - (nc.w - sc.w)/2;
                if(nc.x < 0) { nc.x=0; nc.w=Math.min(nc.w, dim.w); nc.h=nc.w/ratio; nc.y=sc.y+(sc.h-nc.h); }
                else if(nc.x+nc.w > dim.w) { nc.x=Math.max(0, dim.w-nc.w); nc.w=dim.w-nc.x; nc.h=nc.w/ratio; nc.y=sc.y+(sc.h-nc.h); }
            }
        }
    }
    state.crop = nc;
    draw();
}

function dragEnd(){
    if(state.drag.active){
        state.drag.active=false;
        els.cvs.style.cursor='default';
        generateResult();
    }
}

function dataURLToSize(dataURL) {
    // 估算 Base64 字符串的大小 (字节)
    const base64 = dataURL.split(',')[1];
    // DataURL 的 Base64 部分包含 MIME type 占用的字符，但我们只需要估算数据部分
    // Base64 编码：每 3 字节数据转换为 4 字节 Base64 字符
    const padding = (base64.match(/=/g) || []).length;
    // (Base64长度 - padding) * 3/4 得到原始字节数
    const sizeBytes = Math.ceil((base64.length - padding) * 0.75);
    return sizeBytes;
}

function generateResult(){
    if(!state.img)return;
    const c=state.crop;
    const dim=getDim();
    const scaleFactor = parseInt(els.scalePercent.value) / 100;

    // 1. Draw Transformed Full Image to Temp
    const tc=document.createElement('canvas');
    tc.width=dim.w; tc.height=dim.h;
    const tctx=tc.getContext('2d');
    tctx.translate(dim.w/2, dim.h/2);
    tctx.rotate(state.rot*Math.PI/180);
    tctx.scale(state.flipH, state.flipV);
    tctx.drawImage(state.img, -state.img.width/2, -state.img.height/2);
    
    // Original cropped size (pixels to be extracted)
    const originalW = Math.round(c.w);
    const originalH = Math.round(c.h);

    // Final scaled size
    const finalW = Math.round(originalW * scaleFactor);
    const finalH = Math.round(originalH * scaleFactor);

    // 2. Crop and Scale
    const fc=document.createElement('canvas');
    fc.width=finalW; 
    fc.height=finalH;
    const fctx=fc.getContext('2d');
    
    // Draw from temp canvas (full image) onto final canvas, scaling as needed
    fctx.drawImage(tc, c.x, c.y, c.w, c.h, 0, 0, finalW, finalH);
    
    // 3. Generate Data URL (Base64)
    // 确保使用 toDataURL 替代 toBlob，以支持可靠的下载和右键另存为
    const dataUrl = fc.toDataURL('image/jpeg', 0.92);
    state.imgDataUrl = dataUrl;

    // 4. Display & Info
    els.resImg.src=dataUrl;
    
    const sizeBytes = dataURLToSize(dataUrl);
    let sz = sizeBytes/1024;
    let szStr = sz>1024 ? (sz/1024).toFixed(2)+' MB' : sz.toFixed(2)+' KB';
    
    // Display final scaled dimensions and size
    els.resInfo.innerText = `W: ${finalW} | H: ${finalH} | Size: ${szStr}`;
    
    // Update input fields to show the unscaled crop dimension (W/H inputs define the CROP area)
    els.w.value = originalW;
    els.h.value = originalH;
}

function download(){
    // 使用存储的 Base64 Data URL 进行下载
    if(!state.imgDataUrl)return;
    const a=document.createElement('a');
    a.href=state.imgDataUrl;
    const scale = els.scalePercent.value;
    a.download=`cropped_img_${scale}pct.jpg`;
    document.body.appendChild(a); 
    // 关键：通过用户交互触发 click
    a.click(); 
    document.body.removeChild(a);
}

init();
</script>
</body>
</html>